// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.owasp.jxt.compiler.Compiler;
import org.owasp.jxt.servlet.JxtServletBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * JxtC -- A command-line .jxt pre-compiler.  Useful for pre-compiling
 * all .jxt files in a web-app, or just running sanity checks on the
 * files before their put up on a server.
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
public class JxtC {
    static final Logger _log = LoggerFactory.getLogger(JxtEngine.class);

    /**
     * These are the command-line options that are supported.  The
     * enum is used to specify an option's command-line flag
     * (e.g. "-d"), and the internal property that maps to
     * (e.g. "outputDir").  Reflection is then used to set the
     * property when it is encountered on the command line.  The
     * property name is also used as a message key to display a
     * localized message when -help is used.  The reason for an enum
     * (instead of just using reflection directly), is that it gives
     * us control over the order (e.g. for displaying "-help") and
     * allows control over which properties are to be exposed to the
     * command line, and how.
     */
    enum Option {
        HELP("-help", "showHelp"),
        VERBOSE("-v", "verbose"),
        OUTPUT_DIR("-d", "outputDir"),
//        PACKAGE_NAME("-p", "packageName"), // not implemented yet
        WEBROOT("-webroot", "webRoot", true),
        COMPILER("-compiler", "compilerType"),
        TEMPDIR("-tempdir", "tempDir"),
        CLASS_PATH("-classpath", "classPath"),
        CP("-cp", "classPath"),
        COMPILER_SOURCE_VM("-source", "compilerSourceVM"),
        COMPILER_TARGET_VM("-target", "compilerTargetVM"),
        DEBUGGING_INFO("-g", "compilerDebugInfo");

        private static final Map<String,Option> FLAGMAP = new HashMap<String,Option>();
        static {
            try {
                Map<String,PropertyDescriptor> propMap = new HashMap<String,PropertyDescriptor>();
                for (PropertyDescriptor prop : Introspector.getBeanInfo(JxtC.class)
                         .getPropertyDescriptors())
                {
                    propMap.put(prop.getName(), prop);
                }

                for (Option opt : values()) {
                    FLAGMAP.put(opt.flag, opt);
                    opt._propertyDescriptor = propMap.get(opt.propertyName);
                    assert opt._propertyDescriptor != null : "no such property: "+opt.propertyName;
                }
            } catch (IntrospectionException e) {
                _log.error("internal error", e);
                throw (AssertionError)new AssertionError().initCause(e);
            }
        }

        public final String flag;
        public final String propertyName;
        public final boolean required;
        private PropertyDescriptor _propertyDescriptor;

        Option(String flg, String propName) {
            this(flg, propName, false);
        }

        Option(String flg, String propName, boolean req) {
            this.flag = flg;
            this.propertyName = propName;
            this.required = req;
        }

        public static Option forFlag(String flag) {
            return FLAGMAP.get(flag);
        }

        public String getFlagHelp() {
            if (_propertyDescriptor.getPropertyType() == Boolean.TYPE) {
                return flag;
            } else {
                // TODO: localize "<arg>"
                return flag+" <arg>";
            }
        }

        public Object get(JxtC main) {
            try {
                return _propertyDescriptor.getReadMethod().invoke(main);
            } catch (IllegalAccessException e) {
                throw new Error(e);
            } catch (InvocationTargetException e) {
                throw new Error(e);
            }
        }

        public void set(JxtC main, Iterator<String> argIter) {
            try {
                Class<?> type = _propertyDescriptor.getPropertyType();
                Method setter = _propertyDescriptor.getWriteMethod();

                if (Boolean.TYPE == type) {
                    setter.invoke(main, true);
                } else if (String.class == type) {
                    setter.invoke(main, argIter.next());
                } else if (File.class == type) {
                    setter.invoke(main, new File(argIter.next()));
                } else {
                    throw new AssertionError(
                        "Don't know how to handle property type: "+type);
                }
            } catch (IllegalAccessException e) {
                throw new Error(e);
            } catch (InvocationTargetException e) {
                if (e.getCause() instanceof RuntimeException) {
                    throw (RuntimeException)e.getCause();
                } else {
                    throw new Error(e);
                }
            }
        }
    }

    private boolean _verbose;
    private boolean _showHelp;
    private File _outputDir;
    private String _packageName;
    private List<String> _files = new ArrayList<String>();
    private String _compilerType;
    private File _tempDir;
    private String _classPath;
    private File _webRoot;
    private String _compilerSourceVM = "1.5";
    private String _compilerTargetVM = "1.5";
    private boolean _compilerDebugInfo;

    private File _webInf;
    private File _webXmlSource;
    private File _webXmlTarget;
    private JxtEngine _engine;

    public final JxtC parseCommandLine(String[] args) {
        for (Iterator<String> argIter = Arrays.asList(args).iterator() ; argIter.hasNext() ;) {
            String arg = argIter.next();
            if (arg.startsWith("-")) {
                Option opt = Option.forFlag(arg);
                if (opt == null) {
                    throw new IllegalArgumentException(
                        Messages.format("jxtc.unknown-flag", arg));
                }
                opt.set(this, argIter);
            } else {
                _files.add(arg);
            }
        }

        return this;
    }

    private void showHelp() {
        System.out.println(Messages.get("jxtc.usage"));
        int flagWidth = 0;
        for (Option opt : Option.values()) {
            flagWidth = Math.max(opt.getFlagHelp().length(), flagWidth);
        }
        String fmt = "    %-"+flagWidth+"s  %s\n";
        for (Option opt : Option.values()) {
            System.out.printf(
                fmt,
                opt.getFlagHelp(),
                Messages.get("jxtc.option."+opt.propertyName));
        }
    }

    private void showSetup() {
        for (Option opt : Option.values()) {
            System.out.print(opt.propertyName+" = ");
            System.out.println(opt.get(this));
        }

        System.out.println("Files: "+_files);
    }

    private void findFiles(File dir, String path, List<String> files) {
        for (String name : dir.list()) {
            File file = new File(dir, name);
            if (file.isDirectory()) {
                findFiles(file, path + name + File.separator, files);
            } else if (file.isFile() && name.endsWith(JxtEngine.FILE_EXTENSION)) {
                files.add(path + name);
            }
        }
    }

    private Iterable<String> files() {
        if (!_files.isEmpty()) {
            // TODO: rebase absolute files paths to web-app root
            return _files;
        } else {
            List<String> files = new ArrayList<String>();
            findFiles(_webRoot, "", files);
            return files;
        }
    }

    /**
     * Generates the web.xml using the original web.xml and adding in
     * the new servlet mappings.
     *
     * @param servletMap a mapping from jxt file name to generated
     * class.
     */
    private void generateWebXML(Map<String,String> servletMap) {
        try {
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            final Node root = doc.appendChild(doc.createElement("fragment"));

            for (Entry<String,String> entry : servletMap.entrySet()) {
                Node servlet = root.appendChild(doc.createElement("servlet"));
                servlet.appendChild(doc.createElement("servlet-name")).setTextContent(entry.getKey());
                servlet.appendChild(doc.createElement("servlet-class")).setTextContent(entry.getValue());
            }

            for (Entry<String,String> entry : servletMap.entrySet()) {
                Node mapping = root.appendChild(doc.createElement("servlet-mapping"));
                mapping.appendChild(doc.createElement("servlet-name")).setTextContent(entry.getKey());
                mapping.appendChild(doc.createElement("url-pattern")).setTextContent("/"+entry.getKey());
            }

            TransformerFactory factory = TransformerFactory.newInstance();
            factory.setURIResolver(new URIResolver() {
                public Source resolve(String href, String base) throws TransformerException {
                    return new DOMSource(root);
                }
            });

            ByteArrayOutputStream buffer = new ByteArrayOutputStream();

            if (_verbose) {
                System.out.println("Reading "+_webXmlSource);
            }

            // First transform the web.xml through an XSLT that
            // removes the additions of a previous run.
            factory.newTransformer(
                new StreamSource(getClass().getResourceAsStream("jxtc-strip-additions.xsl")))
                .transform(new StreamSource(_webXmlSource),
                           new StreamResult(buffer));

            if (_verbose) {
                System.out.println("Writing "+_webXmlTarget);
            }

            // Then pass the result of that transform into an XSLT
            // that will add in the new mappings.
            factory.newTransformer(
                new StreamSource(getClass().getResourceAsStream("jxtc-webxml.xsl")))
                .transform(new StreamSource(new ByteArrayInputStream(buffer.toByteArray())),
                           new StreamResult(_webXmlTarget));

        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        }
    }

    /**
     * Performs the actual work.  The instance should already be set
     * up by calling parseCommandLine or directly setting the
     * appropriate properties.
     */
    public final void run() {
//         if (_verbose) {
//             showSetup();
//         }

        if (_showHelp) {
            showHelp();
            return;
        }

        if (_webRoot == null) {
            throw new IllegalArgumentException(
                Messages.format("jxtc.missing-required-flag", Option.WEBROOT.flag));
        }

        File outDir = _outputDir;

        if (outDir == null) {
            outDir = new File(_webRoot, "classes");
        }

        _webInf = new File(_webRoot, "WEB-INF");
        _webXmlSource = new File(_webInf, "web.xml");
        _webXmlTarget = new File(_webInf, "web.xml");

        Compiler compiler = Compiler.forName(_compilerType);
        compiler.setSource(_compilerSourceVM);
        compiler.setTarget(_compilerTargetVM);
        compiler.setDebug(_compilerDebugInfo);
        compiler.setClassPath(_classPath);

        try {
            _engine = JxtEngine.builder()
                .webRoot(_webRoot)
                .classPath(_classPath)
                .tempDir(_tempDir)
                .classDir(outDir)
                .compiler(compiler)
                .build();
        } catch (JxtConfigException e) {
            System.out.println("Configuration error: "+e);
            return;
        }

        Map<String,String> servletMap = new LinkedHashMap<String,String>();
        boolean errors = false;

        for (String file : files()) {
            if (_verbose) {
                System.out.print("Compiling "+file+" ");
                System.out.flush();
            }

            try {
                JxtCompilation<JxtServletBase> result = _engine.compileServlet(file);
                for (LocatedMessage warning : result.getWarnings()) {
                    System.out.println(warning);
                }

                if (_verbose) {
                    System.out.println("--> "+result.getTemplateClass().getName());
                }

                servletMap.put(file, result.getTemplateClass().getName());
            } catch (CompileException e) {
                if (!_verbose) {
                    System.out.print("Compiling "+file+" ");
                }
                System.out.println(" errors.");
                System.out.println(e.getMessage());
                errors = true;
            }
        }

        if (errors) {
            System.out.println("Errors encountered, web.xml mappings will not be updated.");
        } else {
            generateWebXML(servletMap);
        }
    }


    public static void main(String[] args) {
        try {
            new JxtC().parseCommandLine(args).run();
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
            System.out.println(Messages.get("jxtc.short-usage"));
        }
    }


    public final boolean isVerbose() {
        return this._verbose;
    }

    public final void setVerbose(final boolean argVerbose) {
        this._verbose = argVerbose;
    }

    public final boolean isShowHelp() {
        return this._showHelp;
    }

    public final void setShowHelp(final boolean argShowHelp) {
        this._showHelp = argShowHelp;
    }

    public final File getOutputDir() {
        return this._outputDir;
    }

    public final void setOutputDir(final File argOutputDir) {
        this._outputDir = argOutputDir;
    }

    public final String getPackageName() {
        return this._packageName;
    }

    public final void setPackageName(final String argPackageName) {
        this._packageName = argPackageName;
    }

    public final String getCompilerType() {
        return this._compilerType;
    }

    public final void setCompilerType(final String argCompilerType) {
        this._compilerType = argCompilerType;
    }

    public final File getTempDir() {
        return this._tempDir;
    }

    public final void setTempDir(final File argTempDir) {
        this._tempDir = argTempDir;
    }

    public final String getClassPath() {
        return this._classPath;
    }

    public final void setClassPath(final String argClassPath) {
        this._classPath = argClassPath;
    }

    public final File getWebRoot() {
        return this._webRoot;
    }

    public final void setWebRoot(final File argWebRoot) {
        this._webRoot = argWebRoot;
    }

    public final String getCompilerSourceVM() {
        return this._compilerSourceVM;
    }

    public final void setCompilerSourceVM(final String argCompilerSourceVM) {
        this._compilerSourceVM = argCompilerSourceVM;
    }

    public final String getCompilerTargetVM() {
        return this._compilerTargetVM;
    }

    public final void setCompilerTargetVM(final String argCompilerTargetVM) {
        this._compilerTargetVM = argCompilerTargetVM;
    }

    public final boolean isCompilerDebugInfo() {
        return this._compilerDebugInfo;
    }

    public final void setCompilerDebugInfo(final boolean argCompilerDebugInfo) {
        this._compilerDebugInfo = argCompilerDebugInfo;
    }

} // JxtC
